Skip to main content

Posts tagged with 'unit testing'

Jeremy Clark is writing unit tests.

Show Notes:

Jeremy Clark is on Twitter.

Want to be on the next episode? You can! All you need is the willingness to talk about something technical.

Theme music is "Crosscutting Concerns" by The Dirty Truckers, check out their music on Amazon or iTunes.

Jesse Riley and I discuss unit testing and how to do it better.

Show Notes:

Jesse Riley is on Twitter

Want to be on the next episode? You can! All you need is the willingness to talk about something technical.

Theme music is "Crosscutting Concerns" by The Dirty Truckers, check out their music on Amazon or iTune

It's the last part of the series, and I just thought I would wrap things up by talking about unit testing the aspect. I wrote a whole chapter in my book AOP in .NET on unit-testing of aspects. I also was honored to do a live webinar with Gael Fraiteur on unit testing with PostSharp, in which he has a different view on aspect unit testing, so I recommend you check that out too.

I'm not straying too much from the book in this example, so if you're familiar with that chapter, there's not much new for you here.

My plan is to not test the PostSharp aspect directly, but instead have the aspect delegate just about all the functionality to a plain old C# class that's easier to test. The old adage of solving a problem by adding another layer of indirection applies. Doing it this way makes my aspect easier to test, further decouples my application from a specific framework, but does require some more work, so it's not like it's a magic cost-free approach to testing either.

Here is the aspect, all tightly coupled to PostSharp:

[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method, TargetMemberAttributes = MulticastAttributes.Public)]
public class ServiceErrorInterceptor : MethodInterceptionAspect
{
public override bool CompileTimeValidate(MethodBase method)
{
if (!typeof (ServiceBase).IsAssignableFrom(method.DeclaringType))
{
var declaringType = method.DeclaringType;
var declaringTypeName = "Unknown";
if (declaringType != null)
declaringTypeName = declaringType.FullName;
Message.Write(method, SeverityType.Error, "SEI01", "The target type (" + declaringTypeName + ") does not implement ServiceBase.");
return false;
}
return base.CompileTimeValidate(method);
}
public override void OnInvoke(MethodInterceptionArgs args)
{
try
{
args.Proceed();
}
catch (SqlException ex)
{
var obj = (ServiceBase) args.Instance;
if (ex.Message.Contains("The DELETE statement conflicted with the REFERENCE constraint"))
obj.AddValidationError("This item can't be deleted because it is still used by other items.");
else
{
obj.AddValidationError(GetErrorMessage(ex));
if (!IsThisSite.RunningLocally)
ex.ToExceptionless().Submit();
}
ErrorReturn(args);
}
catch (NotImplementedException)
{
// if something's not implemented, just throw it up
throw;
}
catch (Exception ex)
{
if (!IsThisSite.RunningLocally)
ex.ToExceptionless().Submit();
var obj = (ServiceBase)args.Instance;
obj.AddValidationError(GetErrorMessage(ex));
ErrorReturn(args);
}
}
private string GetErrorMessage(Exception ex)
{
if(IsThisSite.RunningLocally)
return "There was a database error. Please try again later. (" + ex.Message + "), (" + ex.GetType().FullName + ")";
return "There was a database error. Please try again later.";
}
void ErrorReturn(MethodInterceptionArgs args)
{
try
{
var methodInfo = args.Method as MethodInfo;
if (methodInfo == null)
return;
if (typeof (IEnumerable).IsAssignableFrom(methodInfo.ReturnType) || methodInfo.ReturnType.IsValueType)
if (methodInfo.ReturnType != typeof (void))
args.ReturnValue = Activator.CreateInstance(methodInfo.ReturnType);
else
args.ReturnValue = null;
}
catch
{
// don't want any errors to take place trying to build the correct return value
// so just swallow them, and let args.ReturnValue stay whatever it is
}
}
}

And here is my refactored code, which you'll notice results in a higher number of classes, but it is ready to unit test. You may also notice that some of the logic is a bit different than the above aspect. This is because I found some issues and bugs while I was writing unit tests. So, hooray for unit tests! Also notice the "On" static field. I touch on this in the book, but this flag is merely there so that I can unit test the actual services while isolating them from the aspect. Since PostSharp uses a compile-time weaver, this is necessary. If you are using a runtime AOP weaver like Castle DynamicProxy, that's not necessary.

[Serializable]
[MulticastAttributeUsage(MulticastTargets.Method, TargetMemberAttributes = MulticastAttributes.Public)]
public class ServiceErrorInterceptor : MethodInterceptionAspect
{
[NonSerialized]
ServiceErrorConcern _concern;
public static bool On = true;
public override bool CompileTimeValidate(MethodBase method)
{
if (!typeof (ServiceBase).IsAssignableFrom(method.DeclaringType))
{
var declaringType = method.DeclaringType;
var declaringTypeName = "Unknown";
if (declaringType != null)
declaringTypeName = declaringType.FullName;
Message.Write(method, SeverityType.Error, "SEI01", "The target type (" + declaringTypeName + ") does not implement ServiceBase.");
return false;
}
return base.CompileTimeValidate(method);
}
public override void RuntimeInitialize(MethodBase method)
{
if (!On) return;
// I'm newing up directly instead of using StructureMap/IoC
// because my project structure would require some extra hoops
// but I plan to do that work when it makes sense
// so think of this as a technical debt decision
_concern = new ServiceErrorConcern(new ExceptionlessLogger());
}
public override void OnInvoke(MethodInterceptionArgs args)
{
if (!On)
{
args.Proceed();
return;
}
_concern.OnInvoke(new MethodConcernArgs(args));
}
}
public interface IMethodConcernArgs
{
void Proceed();
object Instance { get; }
MethodBase Method { get; }
object ReturnValue { get; set; }
}
public class MethodConcernArgs : IMethodConcernArgs
{
readonly MethodInterceptionArgs _args;
public MethodConcernArgs(MethodInterceptionArgs args)
{
_args = args;
}
public void Proceed()
{
_args.Proceed();
}
public object Instance
{
get { return _args.Instance; }
}
public MethodBase Method
{
get { return _args.Method; }
}
public object ReturnValue
{
get { return _args.ReturnValue; }
set { _args.ReturnValue = value; }
}
}
public class ServiceErrorConcern
{
readonly IExceptionLogger _exceptionLogger;
public ServiceErrorConcern(IExceptionLogger exceptionLogger)
{
_exceptionLogger = exceptionLogger;
}
public void OnInvoke(IMethodConcernArgs args)
{
try
{
args.Proceed();
}
catch (SqlException ex)
{
var obj = (ServiceBase)args.Instance;
if (ex.Message.Contains("The DELETE statement conflicted with the REFERENCE constraint"))
obj.AddValidationError("This item can't be deleted because it is still used by other items.");
else
{
obj.AddValidationError(GetErrorMessage(ex));
_exceptionLogger.Log(ex);
}
ErrorReturn(args);
}
catch (NotImplementedException)
{
// if something's not implemented, just throw it up
throw;
}
catch (Exception ex)
{
_exceptionLogger.Log(ex);
var obj = (ServiceBase)args.Instance;
obj.AddValidationError(GetErrorMessage(ex));
ErrorReturn(args);
}
}
string GetErrorMessage(Exception ex)
{
if (ex is SqlException)
{
if (IsThisSite.RunningLocally)
return "There was a database error. Please try again later. (" + ex.Message + "), (" + ex.GetType().FullName + ")";
return "There was a database error. Please try again later.";
}
if (IsThisSite.RunningLocally)
return "There was an error. Please try again later. (" + ex.Message + "), (" + ex.GetType().FullName + ")";
return "There was an error. Please try again later.";
}
// if the return type is ienumerable, then I want the return type to be an empty collection
// if the return type is value, then I want the return to be default value
// if the return type is void, don't touch the return value
// if the return type is reference (should cover everything else), then return should be null
void ErrorReturn(IMethodConcernArgs args)
{
try
{
var methodInfo = args.Method as MethodInfo;
if (methodInfo == null)
return;
if (methodInfo.ReturnType == typeof(string))
args.ReturnValue = null;
else if (typeof(IEnumerable).IsAssignableFrom(methodInfo.ReturnType))
args.ReturnValue = Activator.CreateInstance(methodInfo.ReturnType);
else if (methodInfo.ReturnType.IsValueType)
args.ReturnValue = Activator.CreateInstance(methodInfo.ReturnType);
else if (methodInfo.ReturnType != typeof (void))
args.ReturnValue = null;
}
catch
{
// don't want any errors to take place trying to build the correct return value
// so just swallow them, and let args.ReturnValue stay whatever it is
}
}
}

I'll even show you some of my unit tests, which I've written in mspec and JustMock Lite with the help of Sticking Place for testing the SqlException stuff.

// while I'm not actually relying on
// PostSharp or any of my real service classes anywhere in my unit tests
// I still need a service class to stand in and receive errors
// that I can test assertions against
public class FakeService : ServiceBase
{
public List<string> Errors { get; private set; }
public FakeService()
{
Errors = new List<string>();
this.AddValidationError = x => Errors.Add(x);
}
}
view raw FakeService.cs hosted with ❤ by GitHub
public class When_invoking_a_service_method_with_sql_exception
{
static ServiceErrorConcern _concern;
static IMethodConcernArgs _mockArgs;
static FakeService _fakeService;
static SqlException _sqlException;
static IExceptionLogger _mockLogger;
Establish context = () =>
{
_mockLogger = Mock.Create<IExceptionLogger>();
_concern = new ServiceErrorConcern(_mockLogger);
_mockArgs = Mock.Create<IMethodConcernArgs>();
_fakeService = new FakeService();
Mock.Arrange(() => _mockArgs.Instance).Returns(_fakeService);
_sqlException = SqlExceptionHelper.CreateException();
Mock.Arrange(() => _mockArgs.Proceed()).Throws(_sqlException);
};
Because of = () =>
{
_concern.OnInvoke(_mockArgs);
};
It should_callback_with_a_single_constraint_error_message = () =>
_fakeService.Errors.Count.ShouldEqual(1);
It should_callback_with_an_appropriate_error_message = () =>
_fakeService.Errors[0].ShouldContain("There was a database error. Please try again later.");
It should_log_the_exception = () =>
Mock.Assert(() => _mockLogger.Log(Arg.Is(_sqlException)), Occurs.Once());
}
public class When_invoking_a_service_method_with_sql_constraint_exception
{
static ServiceErrorConcern _concern;
static IMethodConcernArgs _mockArgs;
static FakeService _fakeService;
static IExceptionLogger _mockLogger;
static SqlException _sqlException;
Establish context = () =>
{
_mockLogger = Mock.Create<IExceptionLogger>();
_concern = new ServiceErrorConcern(_mockLogger);
_mockArgs = Mock.Create<IMethodConcernArgs>();
_fakeService = new FakeService();
Mock.Arrange(() => _mockArgs.Instance).Returns(_fakeService);
_sqlException = SqlExceptionHelper.CreateException("xxx The DELETE statement conflicted with the REFERENCE constraint xxx");
Mock.Arrange(() => _mockArgs.Proceed()).Throws(_sqlException);
};
Because of = () =>
{
_concern.OnInvoke(_mockArgs);
};
It should_callback_with_a_single_constraint_error_message = () =>
_fakeService.Errors.Count.ShouldEqual(1);
It should_callback_with_an_appropriate_error_message = () =>
_fakeService.Errors[0].ShouldEqual("This item can't be deleted because it is still used by other items.");
It should_not_log_the_exception = () =>
Mock.Assert(() => _mockLogger.Log(Arg.IsAny<Exception>()), Occurs.Never());
}

Notice that I moved the Exceptionless-specific code behind an interface (IExceptionLogger) as well, to further decouple, improve testability, and also because Exceptionless is just the sort of thing that might actually be swapped out somewhere down the line. I've left out the details of that implementation because it's quite trivial.

So now you are caught up to today: this is the aspect I'm actually using and deploying to a staging site for my cofounder and our designer to use and test. And so far I must say that it's working quite well, but things could always change when it actually hits real customers, of course.

Matthew D. Groves

About the Author

Matthew D. Groves lives in Central Ohio. He works remotely, loves to code, and is a Microsoft MVP.

Latest Comments

Twitter